/*-------------------------------------------------------------------------
 * drawElements Quality Program OpenGL ES 2.0 Module
 * -------------------------------------------------
 *
 * Copyright 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 *//*!
 * \file
 * \brief Dithering tests.
 *//*--------------------------------------------------------------------*/

#include "es2fDitheringTests.hpp"
#include "gluRenderContext.hpp"
#include "gluDefs.hpp"
#include "glsFragmentOpUtil.hpp"
#include "gluPixelTransfer.hpp"
#include "tcuRenderTarget.hpp"
#include "tcuRGBA.hpp"
#include "tcuVector.hpp"
#include "tcuPixelFormat.hpp"
#include "tcuTestLog.hpp"
#include "tcuSurface.hpp"
#include "tcuCommandLine.hpp"
#include "deRandom.hpp"
#include "deStringUtil.hpp"
#include "deString.h"
#include "deMath.h"

#include "glw.h"

#include <string>
#include <algorithm>

namespace deqp
{

using tcu::Vec4;
using tcu::IVec4;
using tcu::TestLog;
using gls::FragmentOpUtil::QuadRenderer;
using gls::FragmentOpUtil::Quad;
using tcu::PixelFormat;
using tcu::Surface;
using de::Random;
using std::vector;
using std::string;

namespace gles2
{
namespace Functional
{

static const char* const s_channelNames[4] = { "red", "green", "blue", "alpha" };

static inline IVec4 pixelFormatToIVec4 (const PixelFormat& format)
{
	return IVec4(format.redBits, format.greenBits, format.blueBits, format.alphaBits);
}

template<typename T>
static inline string choiceListStr (const vector<T>& choices)
{
	string result;
	for (int i = 0; i < (int)choices.size(); i++)
	{
		if (i == (int)choices.size()-1)
			result += " or ";
		else if (i > 0)
			result += ", ";
		result += de::toString(choices[i]);
	}
	return result;
}

class DitheringCase : public tcu::TestCase
{
public:
	enum PatternType
	{
		PATTERNTYPE_GRADIENT = 0,
		PATTERNTYPE_UNICOLORED_QUAD,

		PATTERNTYPE_LAST
	};

											DitheringCase				(tcu::TestContext& testCtx, glu::RenderContext& renderCtx, const char* name, const char* description, bool isEnabled, PatternType patternType, const tcu::Vec4& color);
											~DitheringCase				(void);

	IterateResult							iterate						(void);
	void									init						(void);
	void									deinit						(void);

	static const char*						getPatternTypeName			(PatternType type);

private:
	bool									checkColor					(const tcu::Vec4& inputClr, const tcu::RGBA& renderedClr, bool logErrors) const;

	bool									drawAndCheckGradient		(bool isVerticallyIncreasing, const tcu::Vec4& highColor) const;
	bool									drawAndCheckUnicoloredQuad	(const tcu::Vec4& color) const;

	const glu::RenderContext&				m_renderCtx;

	const bool								m_ditheringEnabled;
	const PatternType						m_patternType;
	const tcu::Vec4							m_color;

	const tcu::PixelFormat					m_renderFormat;

	const QuadRenderer*						m_renderer;
	int										m_iteration;
};

const char* DitheringCase::getPatternTypeName (const PatternType type)
{
	switch (type)
	{
		case PATTERNTYPE_GRADIENT:			return "gradient";
		case PATTERNTYPE_UNICOLORED_QUAD:	return "unicolored_quad";
		default:
			DE_ASSERT(false);
			return DE_NULL;
	}
}


DitheringCase::DitheringCase (tcu::TestContext& testCtx, glu::RenderContext& renderCtx, const char* const name, const char* const description, const bool ditheringEnabled, const PatternType patternType, const Vec4& color)
	: TestCase				(testCtx, name, description)
	, m_renderCtx			(renderCtx)
	, m_ditheringEnabled	(ditheringEnabled)
	, m_patternType			(patternType)
	, m_color				(color)
	, m_renderFormat		(renderCtx.getRenderTarget().getPixelFormat())
	, m_renderer			(DE_NULL)
	, m_iteration			(0)
{
}

DitheringCase::~DitheringCase (void)
{
	DitheringCase::deinit();
}

void DitheringCase::init (void)
{
	DE_ASSERT(!m_renderer);
	m_renderer = new QuadRenderer(m_renderCtx, glu::GLSL_VERSION_100_ES);
	m_iteration = 0;
}

void DitheringCase::deinit (void)
{
	delete m_renderer;
	m_renderer = DE_NULL;
}

bool DitheringCase::checkColor (const Vec4& inputClr, const tcu::RGBA& renderedClr, const bool logErrors) const
{
	const IVec4		channelBits		= pixelFormatToIVec4(m_renderFormat);
	bool			allChannelsOk	= true;

	for (int chanNdx = 0; chanNdx < 4; chanNdx++)
	{
		if (channelBits[chanNdx] == 0)
			continue;

		const int		channelMax			= (1 << channelBits[chanNdx]) - 1;
		const float		scaledInput			= inputClr[chanNdx] * (float)channelMax;
		const bool		useRoundingMargin	= deFloatAbs(scaledInput - deFloatRound(scaledInput)) < 0.0001f;
		vector<int>		channelChoices;

		channelChoices.push_back(de::min(channelMax,	(int)deFloatCeil(scaledInput)));
		channelChoices.push_back(de::max(0,				(int)deFloatCeil(scaledInput) - 1));

		// If the input color results in a scaled value that is very close to an integer, account for a little bit of possible inaccuracy.
		if (useRoundingMargin)
		{
			if (scaledInput > deFloatRound(scaledInput))
				channelChoices.push_back((int)deFloatCeil(scaledInput) - 2);
			else
				channelChoices.push_back((int)deFloatCeil(scaledInput) + 1);
		}

		std::sort(channelChoices.begin(), channelChoices.end());

		{
			const int		renderedClrInFormat	= (int)deFloatRound((float)(renderedClr.toIVec()[chanNdx] * channelMax) / 255.0f);
			bool			goodChannel			= false;

			for (int i = 0; i < (int)channelChoices.size(); i++)
			{
				if (renderedClrInFormat == channelChoices[i])
				{
					goodChannel = true;
					break;
				}
			}

			if (!goodChannel)
			{
				if (logErrors)
				{
					m_testCtx.getLog() << TestLog::Message
									   << "Failure: " << channelBits[chanNdx] << "-bit " << s_channelNames[chanNdx] << " channel is " << renderedClrInFormat
									   << ", should be " << choiceListStr(channelChoices)
									   << " (corresponding fragment color channel is " << inputClr[chanNdx] << ")"
									   << TestLog::EndMessage
									   << TestLog::Message
									   << "Note: " << inputClr[chanNdx] << " * (" << channelMax + 1 << "-1) = " << scaledInput
									   << TestLog::EndMessage;

					if (useRoundingMargin)
					{
						m_testCtx.getLog() << TestLog::Message
										   << "Note: one extra color candidate was allowed because fragmentColorChannel * (2^bits-1) is close to an integer"
										   << TestLog::EndMessage;
					}
				}

				allChannelsOk = false;
			}
		}
	}

	return allChannelsOk;
}

bool DitheringCase::drawAndCheckGradient (const bool isVerticallyIncreasing, const Vec4& highColor) const
{
	TestLog&					log					= m_testCtx.getLog();
	Random						rnd					(deStringHash(getName()));
	const int					maxViewportWid		= 256;
	const int					maxViewportHei		= 256;
	const int					viewportWid			= de::min(m_renderCtx.getRenderTarget().getWidth(), maxViewportWid);
	const int					viewportHei			= de::min(m_renderCtx.getRenderTarget().getHeight(), maxViewportHei);
	const int					viewportX			= rnd.getInt(0, m_renderCtx.getRenderTarget().getWidth() - viewportWid);
	const int					viewportY			= rnd.getInt(0, m_renderCtx.getRenderTarget().getHeight() - viewportHei);
	const Vec4					quadClr0			(0.0f, 0.0f, 0.0f, 0.0f);
	const Vec4&					quadClr1			= highColor;
	Quad						quad;
	Surface						renderedImg			(viewportWid, viewportHei);

	GLU_CHECK_CALL(glViewport(viewportX, viewportY, viewportWid, viewportHei));

	log << TestLog::Message << "Dithering is " << (m_ditheringEnabled ? "enabled" : "disabled") << TestLog::EndMessage;

	if (m_ditheringEnabled)
		GLU_CHECK_CALL(glEnable(GL_DITHER));
	else
		GLU_CHECK_CALL(glDisable(GL_DITHER));

	log << TestLog::Message << "Drawing a " << (isVerticallyIncreasing ? "vertically" : "horizontally") << " increasing gradient" << TestLog::EndMessage;

	quad.color[0] = quadClr0;
	quad.color[1] = isVerticallyIncreasing ? quadClr1 : quadClr0;
	quad.color[2] = isVerticallyIncreasing ? quadClr0 : quadClr1;
	quad.color[3] = quadClr1;

	m_renderer->render(quad);

	glu::readPixels(m_renderCtx, viewportX, viewportY, renderedImg.getAccess());
	GLU_CHECK_MSG("glReadPixels()");

	log << TestLog::Image(isVerticallyIncreasing ? "VerGradient"		: "HorGradient",
						  isVerticallyIncreasing ? "Vertical gradient"	: "Horizontal gradient",
						  renderedImg);

	// Validate, at each pixel, that each color channel is one of its two allowed values.

	{
		Surface		errorMask		(viewportWid, viewportHei);
		bool		colorChoicesOk	= true;

		for (int y = 0; y < renderedImg.getHeight(); y++)
		{
			for (int x = 0; x < renderedImg.getWidth(); x++)
			{
				const float		inputF		= ((float)(isVerticallyIncreasing ? y : x) + 0.5f) / (float)(isVerticallyIncreasing ? renderedImg.getHeight() : renderedImg.getWidth());
				const Vec4		inputClr	= (1.0f-inputF)*quadClr0 + inputF*quadClr1;

				if (!checkColor(inputClr, renderedImg.getPixel(x, y), colorChoicesOk))
				{
					errorMask.setPixel(x, y, tcu::RGBA::red());

					if (colorChoicesOk)
					{
						log << TestLog::Message << "First failure at pixel (" << x << ", " << y << ") (not printing further errors)" << TestLog::EndMessage;
						colorChoicesOk = false;
					}
				}
				else
					errorMask.setPixel(x, y, tcu::RGBA::green());
			}
		}

		if (!colorChoicesOk)
		{
			log << TestLog::Image("ColorChoiceErrorMask", "Error mask for color choices", errorMask);
			return false;
		}
	}

	// When dithering is disabled, the color selection must be coordinate-independent - i.e. the colors must be constant in the gradient's constant direction.

	if (!m_ditheringEnabled)
	{
		const int	increasingDirectionSize	= isVerticallyIncreasing ? renderedImg.getHeight() : renderedImg.getWidth();
		const int	constantDirectionSize	= isVerticallyIncreasing ? renderedImg.getWidth() : renderedImg.getHeight();

		for (int incrPos = 0; incrPos < increasingDirectionSize; incrPos++)
		{
			bool		colorHasChanged			= false;
			tcu::RGBA	prevConstantDirectionPix;

			for (int constPos = 0; constPos < constantDirectionSize; constPos++)
			{
				const int			x		= isVerticallyIncreasing ? constPos : incrPos;
				const int			y		= isVerticallyIncreasing ? incrPos : constPos;
				const tcu::RGBA		clr		= renderedImg.getPixel(x, y);

				if (constPos > 0 && clr != prevConstantDirectionPix)
				{
					// Allow color to change once to take into account possibly
					// discontinuity between triangles
					if (colorHasChanged)
					{
						log << TestLog::Message
							<< "Failure: colors should be constant per " << (isVerticallyIncreasing ? "row" : "column")
							<< " (since dithering is disabled), but the color at position (" << x << ", " << y << ") is " << clr
							<< " and does not equal the color at (" << (isVerticallyIncreasing ? x-1 : x) << ", " << (isVerticallyIncreasing ? y : y-1) << "), which is " << prevConstantDirectionPix
							<< TestLog::EndMessage;

						return false;
					}
					else
						colorHasChanged = true;
				}

				prevConstantDirectionPix = clr;
			}
		}
	}

	return true;
}

bool DitheringCase::drawAndCheckUnicoloredQuad (const Vec4& quadColor) const
{
	TestLog&					log					= m_testCtx.getLog();
	Random						rnd					(deStringHash(getName()));
	const int					maxViewportWid		= 32;
	const int					maxViewportHei		= 32;
	const int					viewportWid			= de::min(m_renderCtx.getRenderTarget().getWidth(), maxViewportWid);
	const int					viewportHei			= de::min(m_renderCtx.getRenderTarget().getHeight(), maxViewportHei);
	const int					viewportX			= rnd.getInt(0, m_renderCtx.getRenderTarget().getWidth() - viewportWid);
	const int					viewportY			= rnd.getInt(0, m_renderCtx.getRenderTarget().getHeight() - viewportHei);
	Quad						quad;
	Surface						renderedImg			(viewportWid, viewportHei);

	GLU_CHECK_CALL(glViewport(viewportX, viewportY, viewportWid, viewportHei));

	log << TestLog::Message << "Dithering is " << (m_ditheringEnabled ? "enabled" : "disabled") << TestLog::EndMessage;

	if (m_ditheringEnabled)
		GLU_CHECK_CALL(glEnable(GL_DITHER));
	else
		GLU_CHECK_CALL(glDisable(GL_DITHER));

	log << TestLog::Message << "Drawing an unicolored quad with color " << quadColor << TestLog::EndMessage;

	quad.color[0] = quadColor;
	quad.color[1] = quadColor;
	quad.color[2] = quadColor;
	quad.color[3] = quadColor;

	m_renderer->render(quad);

	glu::readPixels(m_renderCtx, viewportX, viewportY, renderedImg.getAccess());
	GLU_CHECK_MSG("glReadPixels()");

	log << TestLog::Image(("Quad" + de::toString(m_iteration)).c_str(), ("Quad " + de::toString(m_iteration)).c_str(), renderedImg);

	// Validate, at each pixel, that each color channel is one of its two allowed values.

	{
		Surface		errorMask		(viewportWid, viewportHei);
		bool		colorChoicesOk	= true;

		for (int y = 0; y < renderedImg.getHeight(); y++)
		{
			for (int x = 0; x < renderedImg.getWidth(); x++)
			{
				if (!checkColor(quadColor, renderedImg.getPixel(x, y), colorChoicesOk))
				{
					errorMask.setPixel(x, y, tcu::RGBA::red());

					if (colorChoicesOk)
					{
						log << TestLog::Message << "First failure at pixel (" << x << ", " << y << ") (not printing further errors)" << TestLog::EndMessage;
						colorChoicesOk = false;
					}
				}
				else
					errorMask.setPixel(x, y, tcu::RGBA::green());
			}
		}

		if (!colorChoicesOk)
		{
			log << TestLog::Image("ColorChoiceErrorMask", "Error mask for color choices", errorMask);
			return false;
		}
	}

	// When dithering is disabled, the color selection must be coordinate-independent - i.e. the entire rendered image must be unicolored.

	if (!m_ditheringEnabled)
	{
		const tcu::RGBA renderedClr00 = renderedImg.getPixel(0, 0);

		for (int y = 0; y < renderedImg.getHeight(); y++)
		{
			for (int x = 0; x < renderedImg.getWidth(); x++)
			{
				const tcu::RGBA curClr = renderedImg.getPixel(x, y);

				if (curClr != renderedClr00)
				{
					log << TestLog::Message
						<< "Failure: color at (" << x << ", " << y << ") is " << curClr
						<< " and does not equal the color at (0, 0), which is " << renderedClr00
						<< TestLog::EndMessage;

					return false;
				}
			}
		}
	}

	return true;
}

DitheringCase::IterateResult DitheringCase::iterate (void)
{
	if (m_patternType == PATTERNTYPE_GRADIENT)
	{
		// Draw horizontal and vertical gradients.

		DE_ASSERT(m_iteration < 2);

		const bool success = drawAndCheckGradient(m_iteration == 1, m_color);

		if (!success)
		{
			m_testCtx.setTestResult(QP_TEST_RESULT_FAIL, "Fail");
			return STOP;
		}

		if (m_iteration == 1)
		{
			m_testCtx.setTestResult(QP_TEST_RESULT_PASS, "Pass");
			return STOP;
		}
	}
	else if (m_patternType == PATTERNTYPE_UNICOLORED_QUAD)
	{
		const int numQuads = m_testCtx.getCommandLine().getTestIterationCount() > 0 ? m_testCtx.getCommandLine().getTestIterationCount() : 30;

		DE_ASSERT(m_iteration < numQuads);

		const Vec4 quadColor	= (float)m_iteration / (float)(numQuads-1) * m_color;
		const bool success		=  drawAndCheckUnicoloredQuad(quadColor);

		if (!success)
		{
			m_testCtx.setTestResult(QP_TEST_RESULT_FAIL, "Fail");
			return STOP;
		}

		if (m_iteration == numQuads - 1)
		{
			m_testCtx.setTestResult(QP_TEST_RESULT_PASS, "Pass");
			return STOP;
		}
	}
	else
		DE_ASSERT(false);

	m_iteration++;

	return CONTINUE;
}

DitheringTests::DitheringTests (Context& context)
	: TestCaseGroup(context, "dither", "Dithering tests")
{
}

DitheringTests::~DitheringTests (void)
{
}

void DitheringTests::init (void)
{
	static const struct
	{
		const char*		name;
		Vec4			color;
	} caseColors[] =
	{
		{ "white",		Vec4(1.0f, 1.0f, 1.0f, 1.0f) },
		{ "red",		Vec4(1.0f, 0.0f, 0.0f, 1.0f) },
		{ "green",		Vec4(0.0f, 1.0f, 0.0f, 1.0f) },
		{ "blue",		Vec4(0.0f, 0.0f, 1.0f, 1.0f) },
		{ "alpha",		Vec4(0.0f, 0.0f, 0.0f, 1.0f) }
	};

	for (int ditheringEnabledI = 0; ditheringEnabledI <= 1; ditheringEnabledI++)
	{
		const bool				ditheringEnabled	= ditheringEnabledI != 0;
		TestCaseGroup* const	group				= new TestCaseGroup(m_context, ditheringEnabled ? "enabled" : "disabled", "");
		addChild(group);

		for (int patternTypeI = 0; patternTypeI < DitheringCase::PATTERNTYPE_LAST; patternTypeI++)
		{
			for (int caseColorNdx = 0; caseColorNdx < DE_LENGTH_OF_ARRAY(caseColors); caseColorNdx++)
			{
				const DitheringCase::PatternType	patternType		= (DitheringCase::PatternType)patternTypeI;
				const string						caseName		= string("") + DitheringCase::getPatternTypeName(patternType) + "_" + caseColors[caseColorNdx].name;

				group->addChild(new DitheringCase(m_context.getTestContext(), m_context.getRenderContext(), caseName.c_str(), "", ditheringEnabled, patternType, caseColors[caseColorNdx].color));
			}
		}
	}
}

} // Functional
} // gles2
} // deqp
